<?php
require_lib('util/image',false);
class BarcodeException extends Exception {}

class Barcode {
	const COLOR_BG = 0;
	const COLOR_FG = 1;
	const AUTO_LABEL = '##!!AUTO_LABEL!!##';

	protected $colorFg, $colorBg;	
	protected $scale;					// Scale of the graphic, default: 1
	protected $offsetX, $offsetY;		
	protected $labels = array();		
	protected $pushLabel = array(0, 0);	

	const SIZE_SPACING_FONT = 5;

	

	protected $thickness;
	protected $keys, $code;
	protected $positionX;
	protected $textfont;
	protected $text;
	protected $checksumValue;
	protected $displayChecksum;
	protected $label;				
	protected $defaultLabel;		
	
	protected function __construct() {
		$this->setOffsetX(0);
		$this->setOffsetY(0);
		$this->setForegroundColor(0x000000);
		$this->setBackgroundColor(0xffffff);
		$this->setScale(1);
		
		$this->setThickness(30);

		$this->defaultLabel = new GDLabel();
		$this->defaultLabel->set_position(GDLabel::POSITION_BOTTOM);
		$this->setLabel(self::AUTO_LABEL);
		$this->setFont(new GDFont(5));

		$this->text = '';
		$this->checksumValue = false;		
	}

	public function getForegroundColor() {
		return $this->colorFg;
	}

	public function setForegroundColor($code) {
		if ($code instanceof GDColor) {
			$this->colorFg = $code;
		} else {
			$this->colorFg = new GDColor($code);
		}
	}

	public function getBackgroundColor() {
		return $this->colorBg;
	}
	

	public function setBackgroundColor($code) {
		if ($code instanceof GDColor) 	$this->colorBg = $code;
		else	$this->colorBg = new GDColor($code);
		foreach ($this->labels as $label) 	$label->set_bkcolor($this->colorBg);
	}


	public function setColor($fg, $bg) {
		$this->setForegroundColor($fg);
		$this->setBackgroundColor($bg);
	}


	public function getScale() {
		return $this->scale;
	}

	public function setScale($scale) {
		$scale = intval($scale);
		if ($scale <= 0)	$scale=1;
		$this->scale = $scale;
	}


	//public abstract function draw($im);

	/**
	 * [0]->width
	 * [1]->height
	 *
	 * @param int $w
	 * @param int $h
	 * @return int[]
	 */
	public function getDimension($w, $h) {
		$labels = $this->getBiggestLabels(false);
		$pixelsAround = array(0, 0, 0, 0); // TRBL
		if (isset($labels[GDLabel::POSITION_TOP])) {
			$dimension = $labels[GDLabel::POSITION_TOP]->get_dimension();
			$pixelsAround[0] += $dimension[1];
		}

		if (isset($labels[GDLabel::POSITION_RIGHT])) {
			$dimension = $labels[GDLabel::POSITION_RIGHT]->get_dimension();
			$pixelsAround[1] += $dimension[0];
		}

		if (isset($labels[GDLabel::POSITION_BOTTOM])) {
			$dimension = $labels[GDLabel::POSITION_BOTTOM]->get_dimension();
			$pixelsAround[2] += $dimension[1];
		}

		if (isset($labels[GDLabel::POSITION_LEFT])) {
			$dimension = $labels[GDLabel::POSITION_LEFT]->get_dimension();
			$pixelsAround[3] += $dimension[0];
		}

		$finalW = ($w + $this->offsetX) * $this->scale;
		$finalH = ($h + $this->offsetY) * $this->scale;

		// This section will check if a top/bottom label is too big for its width and left/right too big for its height
		$reversedLabels = $this->getBiggestLabels(true);
		foreach ($reversedLabels as $label) {
			$dimension = $label->get_dimension();
			$alignment = $label->alignment;
			if ($label->position === GDLabel::POSITION_LEFT || $label->position === GDLabel::POSITION_RIGHT) {
				if ($alignment === GDLabel::TOP) {
					$pixelsAround[2] = max($pixelsAround[2], $dimension[1] - $finalH);
				} elseif ($alignment === GDLabel::CENTER) {
					$temp = ceil(($dimension[1] - $finalH) / 2);
					$pixelsAround[0] = max($pixelsAround[0], $temp);
					$pixelsAround[2] = max($pixelsAround[2], $temp);
				} elseif ($alignment === GDLabel::BOTTOM) {
					$pixelsAround[0] = max($pixelsAround[0], $dimension[1] - $finalH);
				}
			} else {
				if ($alignment === GDLabel::LEFT) {
					$pixelsAround[1] = max($pixelsAround[1], $dimension[0] - $finalW);
				} elseif ($alignment === GDLabel::CENTER) {
					$temp = ceil(($dimension[0] - $finalW) / 2);
					$pixelsAround[1] = max($pixelsAround[1], $temp);
					$pixelsAround[3] = max($pixelsAround[3], $temp);
				} elseif ($alignment === GDLabel::RIGHT) {
					$pixelsAround[3] = max($pixelsAround[3], $dimension[0] - $finalW);
				}
			}
		}

		$this->pushLabel[0] = $pixelsAround[3];
		$this->pushLabel[1] = $pixelsAround[0];

		$finalW = ($w + $this->offsetX) * $this->scale + $pixelsAround[1] + $pixelsAround[3];
		$finalH = ($h + $this->offsetY) * $this->scale + $pixelsAround[0] + $pixelsAround[2];

		return array($finalW, $finalH);
	}

 
	public function getOffsetX() {
		return $this->offsetX;
	}
 
	public function setOffsetX($offsetX) {
		$offsetX = intval($offsetX);
		if ($offsetX < 0)  $offsetX=0;

		$this->offsetX = $offsetX;
	}
 
	public function getOffsetY() {
		return $this->offsetY;
	}
 
	public function setOffsetY($offsetY) {
		$offsetY = intval($offsetY);
		if ($offsetY < 0)  $offsetY=0;

		$this->offsetY = $offsetY;
	}
 
	public function addLabel(GDLabel $label) {
		$label->set_bkcolor($this->colorBg);
		$this->labels[] = $label;
	}

 
	public function removeLabel(GDLabel $label) {
		$remove = -1;
		$c = count($this->labels);
		for ($i = 0; $i < $c; $i++) {
			if ($this->labels[$i] === $label) {
				$remove = $i;
				break;
			}
		}

		if ($remove > -1) {
			array_splice($this->labels, $remove, 1);
		}
	}
 
	public function clearLabels() {
		$this->labels = array();
	}

	/**
	 * $x1 and $y1 represent the top left corner.
	 * $x2 and $y2 represent the bottom right corner.
	 *
	 * @param resource $im
	 * @param int $x1
	 * @param int $y1
	 * @param int $x2
	 * @param int $y2
	 */
	protected function drawText($im, $x1, $y1, $x2, $y2) {
		foreach ($this->labels as $label) {
			$m=$label->get_dimension();
			$label->draw($im,
				($x1 + $this->offsetX) * $this->scale + $this->pushLabel[0],
				($y1 + $this->offsetY) * $this->scale +$m[1]+ $this->pushLabel[1],
				($x2 + $this->offsetX) * $this->scale + $this->pushLabel[0],
				($y2 + $this->offsetY) * $this->scale+$m[1] + $this->pushLabel[1]);
		}
	}

 
	protected function drawPixel($im, $x, $y, $color = self::COLOR_FG) {
		$xR = ($x + $this->offsetX) * $this->scale + $this->pushLabel[0];
		$yR = ($y + $this->offsetY) * $this->scale + $this->pushLabel[1];

		// We always draw a rectangle
		imagefilledrectangle($im,
			$xR,
			$yR,
			$xR + $this->scale - 1,
			$yR + $this->scale - 1,
			$this->getColor($im, $color));
	}

 
	protected function drawRectangle($im, $x1, $y1, $x2, $y2, $color = self::COLOR_FG) {
		if ($this->scale === 1) {
			imagerectangle($im,
				($x1 + $this->offsetX) * $this->scale + $this->pushLabel[0],
				($y1 + $this->offsetY) * $this->scale + $this->pushLabel[1],
				($x2 + $this->offsetX) * $this->scale + $this->pushLabel[0],
				($y2 + $this->offsetY) * $this->scale + $this->pushLabel[1],
				$this->getColor($im, $color));
		} else {
			imagefilledrectangle($im, ($x1 + $this->offsetX) * $this->scale + $this->pushLabel[0], ($y1 + $this->offsetY) * $this->scale + $this->pushLabel[1], ($x2 + $this->offsetX) * $this->scale + $this->pushLabel[0] + $this->scale - 1, ($y1 + $this->offsetY) * $this->scale + $this->pushLabel[1] + $this->scale - 1, $this->getColor($im, $color));
			imagefilledrectangle($im, ($x1 + $this->offsetX) * $this->scale + $this->pushLabel[0], ($y1 + $this->offsetY) * $this->scale + $this->pushLabel[1], ($x1 + $this->offsetX) * $this->scale + $this->pushLabel[0] + $this->scale - 1, ($y2 + $this->offsetY) * $this->scale + $this->pushLabel[1] + $this->scale - 1, $this->getColor($im, $color));
			imagefilledrectangle($im, ($x2 + $this->offsetX) * $this->scale + $this->pushLabel[0], ($y1 + $this->offsetY) * $this->scale + $this->pushLabel[1], ($x2 + $this->offsetX) * $this->scale + $this->pushLabel[0] + $this->scale - 1, ($y2 + $this->offsetY) * $this->scale + $this->pushLabel[1] + $this->scale - 1, $this->getColor($im, $color));
			imagefilledrectangle($im, ($x1 + $this->offsetX) * $this->scale + $this->pushLabel[0], ($y2 + $this->offsetY) * $this->scale + $this->pushLabel[1], ($x2 + $this->offsetX) * $this->scale + $this->pushLabel[0] + $this->scale - 1, ($y2 + $this->offsetY) * $this->scale + $this->pushLabel[1] + $this->scale - 1, $this->getColor($im, $color));
		}
	}

 
	protected function drawFilledRectangle($im, $x1, $y1, $x2, $y2, $color = self::COLOR_FG) {
		if ($x1 > $x2) { // Swap
			$x1 ^= $x2 ^= $x1 ^= $x2;
		}

		if ($y1 > $y2) { // Swap
			$y1 ^= $y2 ^= $y1 ^= $y2;
		}

		imagefilledrectangle($im,
			($x1 + $this->offsetX) * $this->scale + $this->pushLabel[0],
			($y1 + $this->offsetY) * $this->scale + $this->pushLabel[1],
			($x2 + $this->offsetX) * $this->scale + $this->pushLabel[0] + $this->scale - 1,
			($y2 + $this->offsetY) * $this->scale + $this->pushLabel[1] + $this->scale - 1,
			$this->getColor($im, $color));
	}
 
	function getColor($im, $color) {
		if ($color === self::COLOR_BG)  return $this->colorBg->get_color($im);
		else return $this->colorFg->get_color($im);
	}

 
	private function getBiggestLabels($reversed = false) {
		$searchLR = $reversed ? 1 : 0;
		$searchTB = $reversed ? 0 : 1;
	
		$labels = array();
		foreach ($this->labels as $label) {
			$position = $label->position;
			if (isset($labels[$position])) {
				$savedDimension = $labels[$position]->get_dimension();
				$dimension = $label->get_dimension();
				if ($position === GDLabel::POSITION_LEFT || $position === GDLabel::POSITION_RIGHT) {
					if ($dimension[$searchLR] > $savedDimension[$searchLR]) {
						$labels[$position] = $label;
					}
				} else {
					if ($dimension[$searchTB] > $savedDimension[$searchTB]) {
						$labels[$position] = $label;
					}
				}
			} else {
				$labels[$position] = $label;
			}
		}

		return $labels;
	}

 
	public function getThickness() {
		return $this->thickness;
	}
 
	public function setThickness($thickness) {
		$this->thickness = intval($thickness);
		if ($this->thickness <= 0) $this->thickness=0;
	}

 
	public function getLabel() {
		$label = $this->label;
		if ($this->label === self::AUTO_LABEL) {
			$label = $this->text;
			if ($this->displayChecksum === true && ($checksum = $this->processChecksum()) !== false) {
				$label .= $checksum;
			}
		}

		return $label;
	}

 
	public function setLabel($label) {
		$this->label = $label;
	}

 
	public function getFont() {
		return $this->font;
	}
 
	public function setFont($font) {
		if (is_int($font)) {
			if ($font === 0) {
				$font = null;
			} else {
				$font = new GDFont($font);
			}
		}

		$this->font = $font;
	}

 
	public function parse($text) {
		$this->text = $text;
		$this->checksumValue = false;		// Reset checksumValue
		$this->validate();
		$this->addDefaultLabel();
	}

 
	public function getChecksum() {
		return $this->processChecksum();
	}

 
	public function setDisplayChecksum($displayChecksum) {
		$this->displayChecksum = (bool)$displayChecksum;
	}

 
	protected function addDefaultLabel() {
		$label = $this->getLabel();
		$font = $this->font;
		if ($label !== null && $label !== '' && $font !== null && $this->defaultLabel !== null) {
			$this->defaultLabel->set_font($font);
			$this->defaultLabel->set_text($label);
			$this->addLabel($this->defaultLabel);
		}
	}
 
	protected function validate() {
		// No validation in the abstract class.
	}

 
	protected function findIndex($var) {
		return array_search($var, $this->keys);
	}

 
	protected function findCode($var) {
		return $this->code[$this->findIndex($var)];
	}

 
	protected function drawChar($im, $code, $startBar = true) {
		$colors = array(Barcode::COLOR_FG, Barcode::COLOR_BG);
		$currentColor = $startBar ? 0 : 1;
		$c = strlen($code);
		for ($i = 0; $i < $c; $i++) {
			for ($j = 0; $j < intval($code[$i]) + 1; $j++) {
				$this->drawSingleBar($im, $colors[$currentColor]);
				$this->nextX();
			}

			$currentColor = ($currentColor + 1) % 2;
		}
	}

 
	protected function drawSingleBar($im, $color) {
		$this->drawFilledRectangle($im, $this->positionX, 0, $this->positionX, $this->thickness - 1, $color);
	}

 
	protected function nextX() {
		$this->positionX++;
	}

 
	protected function calculateChecksum() {
		$this->checksumValue = false;
	}

 
	protected function processChecksum() {
		return false;
	}	
	public function output($filename=NULL){
		$drawing = new BarcodeDraw();
		if($filename) $drawing->setFileName($filename);
		else header('Content-Type: image/png');
		$drawing->draw_bar($this);
	}
}

class BarcodeDraw {
	private $dpi;
	private $filename;	

 
	public function setFilename($filename) {
		$this->filename = $filename;
	}
 
	public function setDPI($dpi) {
		if(is_numeric($dpi)) {
			$this->dpi = max(1, $dpi);
		} else {
			$this->dpi = null;
		}
	}
 
	public function draw_bar($bcode) {
		$size = $bcode->getDimension(0, 0);
		$w = max(1, $size[0]);
		$h = max(1, $size[1]);		
		$im = imagecreatetruecolor($w, $h);
		try {
			$color = $bcode->getColor($im, 0 );
			imagefilledrectangle ( $im, 0, 0, $w - 1, $h - 1, $color );
			$bcode->draw($im);
			ob_start ();
			imagepng ( $im );
			
			$bin = ob_get_contents ();
			ob_end_clean ();
			
			$this->setInternalProperties ( $bin );
			
			if (empty ( $this->filename )) {
				echo $bin;
			} else {
				@file_put_contents ( $this->filename, $bin );
			}
			@imagedestroy ( $im );
		} catch ( Exception $e ) {
			@imagedestroy ( $im );
		}
		
	}

	private function setInternalProperties(&$bin) {
		if(strcmp(substr($bin, 0, 8), pack('H*', '89504E470D0A1A0A')) === 0) {
			$chunks = $this->detectChunks($bin);
			$this->internalSetDPI($bin, $chunks);
		}
	}

	private function detectChunks($bin) {
		$data = substr($bin, 8);
		$chunks = array();
		$c = strlen($data);
		
		$offset = 0;
		while($offset < $c) {
			$packed = unpack('Nsize/a4chunk', $data);
			$size = $packed['size'];
			$chunk = $packed['chunk'];

			$chunks[] = array('offset'=>$offset + 8, 'size'=>$size, 'chunk'=>$chunk);
			$jump = $size + 12;
			$offset += $jump;
			$data = substr($data, $jump);
		}
		
		return $chunks;
	}

	private function internalSetDPI(&$bin, &$chunks) {
		if($this->dpi !== null) {
			$meters = (int)($this->dpi * 39.37007874);

			$found = -1;
			$c = count($chunks);
			for($i = 0; $i < $c; $i++) {
				// We already have a pHYs
				if($chunks[$i]['chunk'] === 'pHYs') {
					$found = $i;
					break;
				}
			}

			$data = 'pHYs' . pack('NNC', $meters, $meters, 0x01);
			$crc = self::crc($data, 13);
			$cr = pack('Na13N', 9, $data, $crc);

			// We didn't have a pHYs
			if($found == -1) {
				// Don't do anything if we have a bad PNG
				if($c >= 2 && $chunk[0]['chunk'] = 'IHDR') {
					array_splice($chunks, 1, 0, array(array('offset'=>33, 'size'=>9, 'chunk'=>'pHYs')));

					// Push the data
					for($i = 2; $i < $c; $i++) {
						$chunks[$i]['offset'] += 21;
					}

					$firstPart = substr($bin, 0, 33);
					$secondPart = substr($bin, 33);
					$bin = $firstPart;
					$bin .= $cr;
					$bin .= $secondPart;
				}
			} else {
				$bin = substr_replace($bin, $cr, $chunks[$i]['offset'], 21);
			}
		}
	}

	private static $crc_table = array();
	private static $crc_table_computed = false;

	private static function make_crc_table() {
		for($n = 0; $n < 256; $n++) {
			$c = $n;
			for ($k = 0; $k < 8; $k++) {
				if (($c & 1) == 1) {
					$c = 0xedb88320 ^ (self::SHR($c, 1));
				} else {
					$c = self::SHR($c, 1);
				}
			}
			self::$crc_table[$n] = $c;
		}

		self::$crc_table_computed = true;
	}

	private static function SHR($x, $n) {
		$mask = 0x40000000;

		if ($x < 0) {
			$x &= 0x7FFFFFFF;
			$mask = $mask >> ($n - 1);
			return ($x >> $n) | $mask;
		}

		return (int)$x >> (int)$n;
	}

	private static function update_crc($crc, $buf, $len) {
		$c = $crc;

		if (!self::$crc_table_computed) {
			self::make_crc_table();
		}

		for($n = 0; $n < $len; $n++) {
			$c = self::$crc_table[($c ^ ord($buf[$n])) & 0xff] ^ (self::SHR($c, 8));
		}

		return $c;
	}

	private static function crc($data, $len) {
		return self::update_crc(-1, $data, $len) ^ -1;
	}
}

